package com.apphome.dal.search;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import com.apphome.dal.impl.SearchUtil;


/**
 * The base search DTO (data transfer object). A SearchParameter object is passed into a
 * DAO or Search Processor to define the parameters for a search. There are six
 * types of parameters that can be set.
 * <ul>
 * <li>SearchClass - The Class of the object(s) to search for.
 * <li>Filters - Any number of filters may be specified for the search. Filters
 * specify a property and a condition that it must match for the object to be
 * included in the result. Filters are "ANDed" together by default, but
 * disjunction (OR) can be used instead by setting
 * <code>disjunction == true</code>. See also the {@link Filter} class.
 * <li>Sorts - Any number of sorts may be specified. Each sort consists of a
 * property, a flag for ascending or descending and a flag for whether or not to
 * ignore case. The first sort added is the primary sort, the second, secondary
 * and so on. See also the {@link Sort} class.
 * <li>Fields - By default the entity specified in search class is returned as
 * the result for each row. However, by specifying fields, any combination of
 * individual properties can be returned for each row in the result set. These
 * properties can be returned as maps, lists, arrays or a single object
 * depending on <code>resultMode</code>. See also the {@link Field}
 * class.<br/><br/>
 * 
 * Additionally, fields can be specified using column operators:
 * <code>COUNT, COUNT DISTINCT, SUM, AVG, MAX, MIN</code>. Note that fields with
 * column operators can not be mixed with fields that do not use column
 * operators.<br/><br/>
 * 
 * Set <code>distinct</code> to <code>true</code> in order to filter out
 * duplicate results.<br/><br/>
 * 
 * <li>Fetch - This determines which attached objects to pull along with the
 * base search object. With Hibernate this eagerly loads the specified
 * properties. Otherwise they might be loaded lazily. This is useful for
 * performance and results that will be disconnected from Hibernate and copied
 * to a remote client.
 * <li>Paging - The maximum number of results may be specified with
 * <code>maxResults</code>. (This can also be thought of as results per page.)
 * The first result can be specified using either <code>firstResult</code> or
 * <code>page</code>.
 * </ul>
 * 
 * <code>ISearch</code> is intended to be an immutable interface and only
 * provides getters for each of the properties. The {@link IMutableSearch}
 * interface extends <code>ISearch</code> by adding setters for all the
 * properties.
 * 
 * @see Filter
 * @see Field
 * @see Sort
 * @see IMutableSearch
 * 
 * @author dwolverton
 * 
 */
public class SearchParameter implements Serializable {

	private static final long serialVersionUID = 1L;
	
	
	/**
	 * Value for result mode. This is the default value. With
	 * <code>RESULT_AUTO</code> the result mode is automatically determined
	 * according to the following rules:
	 * <ul>
	 * <li>If any field is specified with a key, use <code>RESULT_MAP</code>.
	 * <li>Otherwise, if zero or one fields are specified, use <code>
	 * RESULT_SINGLE</code>.
	 * <li>Otherwise, use <code>RESULT_ARRAY</code>.
	 * </ul>
	 * 
	 * @see #getResultMode()
	 */
	public static final int RESULT_AUTO = 0;

	/**
	 * Value for result mode. <code>RESULT_ARRAY</code> returns each result as
	 * an Object array (<code>Object[]</code>) with the entries corresponding to
	 * the fields added to the search. Here's an example:
	 * 
	 * <pre>
	 * Search s = new Search(Person.class);
	 * s.setResultMode(Search.RESULT_ARRAY);
	 * s.addField(&quot;firstName&quot;);
	 * s.addField(&quot;lastName&quot;);
	 * for (Object[] array : dao.search(s)) {
	 * 	System.out.println(array[0] + &quot; &quot; + array[1]);
	 * }
	 * </pre>
	 * 
	 * @see #getResultMode()
	 */
	public static final int RESULT_ARRAY = 1;

	/**
	 * Value for result mode. <code>RESULT_LIST</code> returns each result as a
	 * list of Objects (<code>List&lt;Object&gt;</Code> ). Here's an example:
	 * 
	 * <pre>
	 * Search s = new Search(Person.class);
	 * s.setResultMode(Search.RESULT_LIST);
	 * s.addField(&quot;firstName&quot;);
	 * s.addField(&quot;lastName&quot;);
	 * for (List&lt;Object&gt; list : dao.search(s)) {
	 * 	System.out.println(list.get(0) + &quot; &quot; + list.get(1));
	 * }
	 * </pre>
	 * 
	 * @see #getResultMode()
	 */
	public static final int RESULT_LIST = 2;

	/**
	 * Value for result mode. <code>RESULT_MAP</code> returns each row as a map
	 * with properties' names or keys for keys to the corresponding values.
	 * Here's an example:
	 * 
	 * <pre>
	 * Search s = new Search(Person.class);
	 * s.setResultMode(Search.RESULT_MAP;
	 * s.addField(&quot;firstName&quot;);
	 * s.addField(&quot;lastName&quot;, &quot;ln&quot;);
	 * for (Map&lt;String, Object&gt; map : dao.search(s)) {
	 * 	System.out.println(map.get(&quot;firstName&quot;) + &quot; &quot; + map.get(&quot;ln&quot;));
	 * }
	 * </pre>
	 * 
	 * @see #getResultMode()
	 */
	public static final int RESULT_MAP = 3;

	/**
	 * Value for result mode. <code>RESULT_SINGLE</code> - Exactly one field or
	 * no fields must be specified to use this result mode. The result list
	 * contains just the value of that property for each element. Here's an
	 * example:
	 * 
	 * <pre>
	 * Search s = new Search(Person.class);
	 * s.setResultMode(Search.RESULT_SINGLE);
	 * s.addField(&quot;firstName&quot;);
	 * for (Object name : dao.search(s)) {
	 * 	System.out.println(name);
	 * }
	 * </pre>
	 * 
	 * @see #getResultMode()
	 */
	public static final int RESULT_SINGLE = 4;

	protected int firstResult = -1; // -1 stands for unspecified

	protected int maxResults = -1; // -1 stands for unspecified

	protected int page = -1; // -1 stands for unspecified

	protected Class<?> searchClass;

	protected List<Filter> filters = new ArrayList<Filter>();

	protected boolean disjunction;

	protected List<Sort> sorts = new ArrayList<Sort>();

	protected List<Field> fields = new ArrayList<Field>();
	
	protected boolean distinct;

	protected List<String> fetches = new ArrayList<String>();

	protected int resultMode = RESULT_AUTO;

	public SearchParameter() {

	}

	public SearchParameter(Class<?> searchClass) {
		this.searchClass = searchClass;
	}

	public SearchParameter setSearchClass(Class<?> searchClass) {
		this.searchClass = searchClass;
		return this;
	}

	public Class<?> getSearchClass() {
		return searchClass;
	}

	// Filters
	public SearchParameter addFilter(Filter filter) {
		SearchUtil.addFilter(this, filter);
		return this;
	}
	
	public SearchParameter addFilters(Filter... filters) {
		SearchUtil.addFilters(this, filters);
		return this;
	}

	/**
	 * Add a filter that uses the == operator.
	 */
	public SearchParameter addFilterEqual(String property, Object value) {
		SearchUtil.addFilterEqual(this, property, value);
		return this;
	}

	/**
	 * Add a filter that uses the >= operator.
	 */
	public SearchParameter addFilterGreaterOrEqual(String property, Object value) {
		SearchUtil.addFilterGreaterOrEqual(this, property, value);
		return this;
	}

	/**
	 * Add a filter that uses the > operator.
	 */
	public SearchParameter addFilterGreaterThan(String property, Object value) {
		SearchUtil.addFilterGreaterThan(this, property, value);
		return this;
	}

	/**
	 * Add a filter that uses the IN operator.
	 */
	public SearchParameter addFilterIn(String property, Collection<?> value) {
		SearchUtil.addFilterIn(this, property, value);
		return this;
	}

	/**
	 * Add a filter that uses the IN operator.
	 * 
	 * <p>
	 * This takes a variable number of parameters. Any number of values can be
	 * specified.
	 */
	public SearchParameter addFilterIn(String property, Object... value) {
		SearchUtil.addFilterIn(this, property, value);
		return this;
	}

	/**
	 * Add a filter that uses the NOT IN operator.
	 */
	public SearchParameter addFilterNotIn(String property, Collection<?> value) {
		SearchUtil.addFilterNotIn(this, property, value);
		return this;
	}

	/**
	 * Add a filter that uses the NOT IN operator.
	 * 
	 * <p>
	 * This takes a variable number of parameters. Any number of values can be
	 * specified.
	 */
	public SearchParameter addFilterNotIn(String property, Object... value) {
		SearchUtil.addFilterNotIn(this, property, value);
		return this;
	}

	/**
	 * Add a filter that uses the <= operator.
	 */
	public SearchParameter addFilterLessOrEqual(String property, Object value) {
		SearchUtil.addFilterLessOrEqual(this, property, value);
		return this;
	}

	/**
	 * Add a filter that uses the < operator.
	 */
	public SearchParameter addFilterLessThan(String property, Object value) {
		SearchUtil.addFilterLessThan(this, property, value);
		return this;
	}

	/**
	 * Add a filter that uses the LIKE operator.
	 */
	public SearchParameter addFilterLike(String property, String value) {
		SearchUtil.addFilterLike(this, property, value);
		return this;
	}

	/**
	 * Add a filter that uses the ILIKE operator.
	 */
	public SearchParameter addFilterILike(String property, String value) {
		SearchUtil.addFilterILike(this, property, value);
		return this;
	}

	/**
	 * Add a filter that uses the != operator.
	 */
	public SearchParameter addFilterNotEqual(String property, Object value) {
		SearchUtil.addFilterNotEqual(this, property, value);
		return this;
	}

	/**
	 * Add a filter that uses the IS NULL operator.
	 */
	public SearchParameter addFilterNull(String property) {
		SearchUtil.addFilterNull(this, property);
		return this;
	}

	/**
	 * Add a filter that uses the IS NOT NULL operator.
	 */
	public SearchParameter addFilterNotNull(String property) {
		SearchUtil.addFilterNotNull(this, property);
		return this;
	}

	/**
	 * Add a filter that uses the IS EMPTY operator.
	 */
	public SearchParameter addFilterEmpty(String property) {
		SearchUtil.addFilterEmpty(this, property);
		return this;
	}
	
	/**
	 * Add a filter that uses the IS NOT EMPTY operator.
	 */
	public SearchParameter addFilterNotEmpty(String property) {
		SearchUtil.addFilterNotEmpty(this, property);
		return this;
	}
	
	/**
	 * Add a filter that uses the AND operator.
	 * 
	 * <p>
	 * This takes a variable number of parameters. Any number of <code>Filter
	 * </code>s can be
	 * specified.
	 */
	public SearchParameter addFilterAnd(Filter... filters) {
		SearchUtil.addFilterAnd(this, filters);
		return this;
	}

	/**
	 * Add a filter that uses the OR operator.
	 * 
	 * <p>
	 * This takes a variable number of parameters. Any number of <code>Filter
	 * </code>s can be
	 * specified.
	 */
	public SearchParameter addFilterOr(Filter... filters) {
		SearchUtil.addFilterOr(this, filters);
		return this;
	}

	/**
	 * Add a filter that uses the NOT operator.
	 */
	public SearchParameter addFilterNot(Filter filter) {
		SearchUtil.addFilterNot(this, filter);
		return this;
	}

	/**
	 * Add a filter that uses the SOME operator.
	 */
	public SearchParameter addFilterSome(String property, Filter filter) {
		SearchUtil.addFilterSome(this, property, filter);
		return this;
	}
	
	/**
	 * Add a filter that uses the ALL operator.
	 */
	public SearchParameter addFilterAll(String property, Filter filter) {
		SearchUtil.addFilterAll(this, property, filter);
		return this;
	}
	
	/**
	 * Add a filter that uses the NONE operator.
	 */
	public SearchParameter addFilterNone(String property, Filter filter) {
		SearchUtil.addFilterNone(this, property, filter);
		return this;
	}
	
	/**
	 * Add a filter that uses a custom expression.
	 * 
	 * @see {@link Filter#custom(String)}
	 */
	public SearchParameter addFilterCustom(String expression) {
		SearchUtil.addFilterCustom(this, expression);
		return this;
	}

	/**
	 * Add a filter that uses a custom expression.
	 * 
	 * @see {@link Filter#custom(String, Object...)}
	 */
	public SearchParameter addFilterCustom(String expression, Object... values) {
		SearchUtil.addFilterCustom(this, expression, values);
		return this;
	}
	
	/**
	 * Add a filter that uses a custom expression.
	 * 
	 * @see {@link Filter#custom(String, Collection)}
	 */
	public SearchParameter addFilterCustom(String expression, Collection<?> values) {
		SearchUtil.addFilterCustom(this, expression, values);
		return this;
	}
	
	public void removeFilter(Filter filter) {
		SearchUtil.removeFilter(this, filter);
	}

	/**
	 * Remove all filters on the given property.
	 */
	public void removeFiltersOnProperty(String property) {
		SearchUtil.removeFiltersOnProperty(this, property);
	}

	public void clearFilters() {
		SearchUtil.clearFilters(this);
	}

	public boolean isDisjunction() {
		return disjunction;
	}

	/**
	 * Filters added to a search are "ANDed" together if this is false (default)
	 * and "ORed" if it is set to true.
	 */
	public SearchParameter setDisjunction(boolean disjunction) {
		this.disjunction = disjunction;
		return this;
	}

	// Sorts
	public SearchParameter addSort(Sort sort) {
		SearchUtil.addSort(this, sort);
		return this;
	}
	
	public SearchParameter addSorts(Sort... sorts) {
		SearchUtil.addSorts(this, sorts);
		return this;
	}

	/**
	 * Add ascending sort by property
	 */
	public SearchParameter addSortAsc(String property) {
		SearchUtil.addSortAsc(this, property);
		return this;
	}

	/**
	 * Add ascending sort by property
	 */
	public SearchParameter addSortAsc(String property, boolean ignoreCase) {
		SearchUtil.addSortAsc(this, property, ignoreCase);
		return this;
	}

	/**
	 * Add descending sort by property
	 */
	public SearchParameter addSortDesc(String property) {
		SearchUtil.addSortDesc(this, property);
		return this;
	}

	/**
	 * Add descending sort by property
	 */
	public SearchParameter addSortDesc(String property, boolean ignoreCase) {
		SearchUtil.addSortDesc(this, property, ignoreCase);
		return this;
	}

	/**
	 * Add sort by property. Ascending if <code>desc == false</code>, descending
	 * if <code>desc == true</code>.
	 */
	public SearchParameter addSort(String property, boolean desc) {
		SearchUtil.addSort(this, property, desc);
		return this;
	}

	/**
	 * Add sort by property. Ascending if <code>desc == false</code>, descending
	 * if <code>desc == true</code>.
	 */
	public SearchParameter addSort(String property, boolean desc, boolean ignoreCase) {
		SearchUtil.addSort(this, property, desc, ignoreCase);
		return this;
	}

	public void removeSort(Sort sort) {
		SearchUtil.removeSort(this, sort);
	}

	public void removeSort(String property) {
		SearchUtil.removeSort(this, property);
	}

	public void clearSorts() {
		SearchUtil.clearSorts(this);
	}

	// Fields
	public SearchParameter addField(Field field) {
		SearchUtil.addField(this, field);
		return this;
	}
	
	public SearchParameter addFields(Field... fields) {
		SearchUtil.addFields(this, fields);
		return this;
	}

	/**
	 * If this field is used with <code>resultMode == RESULT_MAP</code>, the
	 * <code>property</code> will also be used as the key for this value in the
	 * map.
	 */
	public SearchParameter addField(String property) {
		SearchUtil.addField(this, property);
		return this;
	}

	/**
	 * If this field is used with <code>resultMode == RESULT_MAP</code>, the
	 * <code>key</code> will be used as the key for this value in the map.
	 */
	public SearchParameter addField(String property, String key) {
		SearchUtil.addField(this, property, key);
		return this;
	}

	/**
	 * If this field is used with <code>resultMode == RESULT_MAP</code>, the
	 * <code>property</code> will also be used as the key for this value in the
	 * map.
	 */
	public SearchParameter addField(String property, int operator) {
		SearchUtil.addField(this, property, operator);
		return this;
	}

	/**
	 * If this field is used with <code>resultMode == RESULT_MAP</code>, the
	 * <code>key</code> will be used as the key for this value in the map.
	 */
	public SearchParameter addField(String property, int operator, String key) {
		SearchUtil.addField(this, property, operator, key);
		return this;
	}

	public void removeField(Field field) {
		SearchUtil.removeField(this, field);
	}

	public void removeField(String property) {
		SearchUtil.removeField(this, property);
	}

	public void removeField(String property, String key) {
		SearchUtil.removeField(this, property, key);
	}

	public void clearFields() {
		SearchUtil.clearFields(this);
	}
	
	public boolean isDistinct() {
		return distinct;
	}
	
	public void setDistinct(boolean distinct) {
		this.distinct = distinct;
	}

	public int getResultMode() {
		return resultMode;
	}

	public SearchParameter setResultMode(int resultMode) {
		if (resultMode < 0 || resultMode > 4)
			throw new IllegalArgumentException("Result Mode ( " + resultMode + " ) is not a valid option.");
		this.resultMode = resultMode;
		return this;
	}

	// Fetches
	public SearchParameter addFetch(String property) {
		SearchUtil.addFetch(this, property);
		return this;
	}
	
	public SearchParameter addFetches(String... properties) {
		SearchUtil.addFetches(this, properties);
		return this;
	}

	public void removeFetch(String property) {
		SearchUtil.removeFetch(this, property);
	}

	public void clearFetches() {
		SearchUtil.clearFetches(this);
	}

	public void clear() {
		SearchUtil.clear(this);
	}

	// Paging
	public int getFirstResult() {
		return firstResult;
	}

	public SearchParameter setFirstResult(int firstResult) {
		this.firstResult = firstResult;
		return this;
	}

	public int getPage() {
		return page;
	}

	public SearchParameter setPage(int page) {
		this.page = page;
		return this;
	}

	public int getMaxResults() {
		return maxResults;
	}

	public SearchParameter setMaxResults(int maxResults) {
		this.maxResults = maxResults;
		return this;
	}

	public void clearPaging() {
		SearchUtil.clearPaging(this);
	}

	/**
	 * Create a copy of this search. All collections are copied into new
	 * collections, but them items in those collections are not duplicated; they
	 * still point to the same objects.
	 */
	public SearchParameter copy() {
		SearchParameter dest = new SearchParameter();
		SearchUtil.copy(this, dest);
		return dest;
	}

	@Override
	public boolean equals(Object obj) {
		return SearchUtil.equals(this, obj);
	}

	@Override
	public int hashCode() {
		return SearchUtil.hashCode(this);
	}

	@Override
	public String toString() {
		return SearchUtil.toString(this);
	}

	public List<Filter> getFilters() {
		return filters;
	}

	public SearchParameter setFilters(List<Filter> filters) {
		this.filters = filters;
		return this;
	}

	public List<Sort> getSorts() {
		return sorts;
	}

	public SearchParameter setSorts(List<Sort> sorts) {
		this.sorts = sorts;
		return this;
	}

	public List<Field> getFields() {
		return fields;
	}

	public SearchParameter setFields(List<Field> fields) {
		this.fields = fields;
		return this;
	}

	public List<String> getFetches() {
		return fetches;
	}

	public SearchParameter setFetches(List<String> fetches) {
		this.fetches = fetches;
		return this;
	}

}
